﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ESRI.ArcGIS.Server;
using ESRI.ArcGIS.ADF.ArcGISServer;
using ESRI.ArcGIS.Geometry;

namespace WorldViewSolutions.ArcGISServer.SOE
{
    /// <summary>
    /// Various static methods for creating spatial objects.
    /// </summary>
    /// <remarks></remarks>
    public static class SpatialUtilities
    {
        /// <summary>
        /// Creates the COM envelope with a null spatial reference
        /// </summary>
        /// <param name="xMin">Bottom-Left X coordinate of the envelope.</param>
        /// <param name="yMin">Bottom-Left Y coordinate of the envelope.</param>
        /// <param name="xMax">Top-Right X coordinate of the envelope.</param>
        /// <param name="yMax">Top-Right Y coordinate of the envelope.</param>
        /// <returns>New envelope instance.</returns>
        public static IEnvelope CreateCOMEnvelope(double xMin, double yMin, double xMax, double yMax)
        {
            return CreateCOMEnvelope(xMin, yMin, xMax, yMax, null);
        }

        /// <summary>
        /// Creates the COM envelope.
        /// </summary>
        /// <param name="xMin">Bottom-Left X coordinate of the envelope.</param>
        /// <param name="yMin">Bottom-Left Y coordinate of the envelope.</param>
        /// <param name="xMax">Top-Right X coordinate of the envelope.</param>
        /// <param name="yMax">Top-Right Y coordinate of the envelope.</param>
        /// <param name="spatialReference">The spatial reference.</param>
        /// <returns>New envelope instance.</returns>
        public static IEnvelope CreateCOMEnvelope(double xMin, double yMin, double xMax, double yMax, ISpatialReference spatialReference)
        {
            IEnvelope env = new ESRI.ArcGIS.Geometry.EnvelopeClass();
            env.XMin = xMin;
            env.YMin = yMin;
            env.XMax = xMax;
            env.YMax = yMax;

            if (spatialReference != null)
                env.SpatialReference = spatialReference;

            return env;
        }

        /// <summary>
        /// Creates the AGS envelope.
        /// </summary>
        /// <param name="xMin">Bottom-Left X coordinate of the envelope.</param>
        /// <param name="yMin">Bottom-Left Y coordinate of the envelope.</param>
        /// <param name="xMax">Top-Right X coordinate of the envelope.</param>
        /// <param name="yMax">Top-Right Y coordinate of the envelope.</param>
        /// <returns>New envelope instance.</returns>
        public static ESRI.ArcGIS.ADF.ArcGISServer.EnvelopeN CreateAGSEnvelope(double xMin, double yMin, double xMax, double yMax)
        {
            return CreateAGSEnvelope(xMin, yMin, xMax, yMax, null);
        }

        /// <summary>
        /// Creates the AGS envelope.
        /// </summary>
        /// <param name="xMin">Bottom-Left X coordinate of the envelope.</param>
        /// <param name="yMin">Bottom-Left Y coordinate of the envelope.</param>
        /// <param name="xMax">Top-Right X coordinate of the envelope.</param>
        /// <param name="yMax">Top-Right Y coordinate of the envelope.</param>
        /// <param name="spatialReference">The spatial reference.</param>
        /// <returns>New envelope instance.</returns>
        public static ESRI.ArcGIS.ADF.ArcGISServer.EnvelopeN CreateAGSEnvelope(double xMin, double yMin, double xMax, double yMax, ESRI.ArcGIS.ADF.ArcGISServer.SpatialReference spatialReference)
        {
            ESRI.ArcGIS.ADF.ArcGISServer.EnvelopeN env = new ESRI.ArcGIS.ADF.ArcGISServer.EnvelopeN();
            env.XMin = xMin;
            env.YMin = yMin;
            env.XMax = xMax;
            env.YMax = yMax;

            if (spatialReference != null)
                env.SpatialReference = spatialReference;

            return env;
        }

        /// <summary>
        /// Creates the COM spatial reference.
        /// </summary>
        /// <param name="wkid">The wkid.</param>
        /// <returns>New spatial reference instance.</returns>
        public static ISpatialReference CreateCOMSpatialReference(int wkid)
        {
            return CreateCOMSpatialReference(wkid, null);
        }

        /// <summary>
        /// Creates the COM spatial reference.
        /// </summary>
        /// <param name="wkt">The WKT.</param>
        /// <returns>New spatial reference instance.</returns>
        public static ISpatialReference CreateCOMSpatialReference(string wkt)
        {
            return CreateCOMSpatialReference(null, wkt);
        }

        /// <summary>
        /// Creates the COM spatial reference.
        /// </summary>
        /// <param name="wkid">The wkid.</param>
        /// <param name="wkt">The WKT.</param>
        /// <returns>New spatial reference instance.</returns>
        private static ISpatialReference CreateCOMSpatialReference(int? wkid, string wkt)
        {
            // create the spatial reference
            ESRI.ArcGIS.Geometry.ISpatialReference iSpatialReference;

            ISpatialReferenceFactory3 spatialReferenceFactory = new SpatialReferenceEnvironmentClass();


            int cBytesRead;

            if (wkid.HasValue)
            {
                ESRI.ArcGIS.ADF.Web.SpatialReference.IDSpatialReferenceInfo srIDInfo = new ESRI.ArcGIS.ADF.Web.SpatialReference.IDSpatialReferenceInfo(wkid.Value);
                try
                {
                    ESRI.ArcGIS.Geometry.IProjectedCoordinateSystem iPrj = spatialReferenceFactory.CreateProjectedCoordinateSystem(srIDInfo.ID);
                    iSpatialReference = iPrj as ESRI.ArcGIS.Geometry.ISpatialReference;
                }
                catch
                {
                    ESRI.ArcGIS.Geometry.IGeographicCoordinateSystem iGeo = spatialReferenceFactory.CreateGeographicCoordinateSystem(srIDInfo.ID);
                    iSpatialReference = iGeo as ESRI.ArcGIS.Geometry.ISpatialReference;
                }
            }
            else
            {
                ESRI.ArcGIS.ADF.Web.SpatialReference.DefinitionSpatialReferenceInfo srDInfo = new ESRI.ArcGIS.ADF.Web.SpatialReference.DefinitionSpatialReferenceInfo(wkt);
                spatialReferenceFactory.CreateESRISpatialReference(srDInfo.Definition, out iSpatialReference, out cBytesRead);
            }

            return iSpatialReference;
        }

        /// <summary>
        /// Creates the AGS spatial reference.
        /// </summary>
        /// <param name="wkt">The WKT.</param>
        /// <returns>New spatial reference instance.</returns>
        public static ESRI.ArcGIS.ADF.ArcGISServer.SpatialReference CreateAGSSpatialReference(string wkt)
        {
            ESRI.ArcGIS.ADF.ArcGISServer.SpatialReference agsSpatialReference;
            ESRI.ArcGIS.Geometry.ISpatialReference comSpatialReference = CreateCOMSpatialReference(wkt);

            if (comSpatialReference is ESRI.ArcGIS.Geometry.ProjectedCoordinateSystem)
            {
                agsSpatialReference = new ESRI.ArcGIS.ADF.ArcGISServer.ProjectedCoordinateSystem();
            }
            else
            {
                agsSpatialReference = new ESRI.ArcGIS.ADF.ArcGISServer.GeographicCoordinateSystem();
            }
            agsSpatialReference.WKT = wkt;
            return agsSpatialReference;
        }

        /// <summary>
        /// Creates the AGS spatial reference.
        /// </summary>
        /// <param name="wkid">The wkid.</param>
        /// <returns>New spatial reference instance.</returns>
        public static ESRI.ArcGIS.ADF.ArcGISServer.SpatialReference CreateAGSSpatialReference(int wkid)
        {
            ESRI.ArcGIS.ADF.ArcGISServer.SpatialReference agsSpatialReference;
            ESRI.ArcGIS.Geometry.ISpatialReference comSpatialReference = CreateCOMSpatialReference(wkid);

            if (comSpatialReference is ESRI.ArcGIS.Geometry.ProjectedCoordinateSystem)
            {
                agsSpatialReference = new ESRI.ArcGIS.ADF.ArcGISServer.ProjectedCoordinateSystem();
            }
            else
            {
                agsSpatialReference = new ESRI.ArcGIS.ADF.ArcGISServer.GeographicCoordinateSystem();
            }

            agsSpatialReference.WKID = wkid;
            agsSpatialReference.WKIDSpecified = true;
            return agsSpatialReference;
        }

        /// <summary>
        /// Gets the map units from spatial reference.
        /// </summary>
        /// <param name="spatialReference">The spatial reference.</param>
        /// <param name="isDegrees">True when the spatial reference is in decimal degrees.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public static double GetMapUnitsFromSpatialReference(SpatialReference spatialReference, out bool isDegrees)
        {
            isDegrees = false;
            double conversionFactor = -1;
            ESRI.ArcGIS.Geometry.ISpatialReference iSpatialReference;


            ESRI.ArcGIS.Geometry.ISpatialReferenceFactory srFactory = new ESRI.ArcGIS.Geometry.SpatialReferenceEnvironmentClass();
            int cBytesRead;

            if (spatialReference.WKIDSpecified)
            {
                try
                {
                    ESRI.ArcGIS.Geometry.IProjectedCoordinateSystem iPrj = srFactory.CreateProjectedCoordinateSystem(spatialReference.WKID);

                    if (Enum.IsDefined(typeof(ESRI.ArcGIS.Geometry.esriSRUnitType), iPrj.CoordinateUnit.FactoryCode))
                    {
                        ESRI.ArcGIS.Geometry.esriSRUnitType myUnit = (ESRI.ArcGIS.Geometry.esriSRUnitType)Enum.ToObject(typeof(ESRI.ArcGIS.Geometry.esriSRUnitType), iPrj.CoordinateUnit.FactoryCode);
                        if (myUnit == ESRI.ArcGIS.Geometry.esriSRUnitType.esriSRUnit_Degree) isDegrees = true;
                    }
                    else if (Enum.IsDefined(typeof(ESRI.ArcGIS.Geometry.esriSRUnit2Type), iPrj.CoordinateUnit.FactoryCode))
                    {
                        ESRI.ArcGIS.Geometry.esriSRUnit2Type myUnit2 = (ESRI.ArcGIS.Geometry.esriSRUnit2Type)Enum.ToObject(typeof(ESRI.ArcGIS.Geometry.esriSRUnit2Type), iPrj.CoordinateUnit.FactoryCode);
                    }

                    conversionFactor = iPrj.CoordinateUnit.ConversionFactor;


                }
                catch
                {
                    ESRI.ArcGIS.Geometry.IGeographicCoordinateSystem iGeo = srFactory.CreateGeographicCoordinateSystem(spatialReference.WKID);
                    if (Enum.IsDefined(typeof(ESRI.ArcGIS.Geometry.esriSRUnitType), iGeo.CoordinateUnit.FactoryCode))
                    {
                        ESRI.ArcGIS.Geometry.esriSRUnitType myUnit = (ESRI.ArcGIS.Geometry.esriSRUnitType)Enum.ToObject(typeof(ESRI.ArcGIS.Geometry.esriSRUnitType), iGeo.CoordinateUnit.FactoryCode);
                        if (myUnit == ESRI.ArcGIS.Geometry.esriSRUnitType.esriSRUnit_Degree) isDegrees = true;
                    }
                    else if (Enum.IsDefined(typeof(ESRI.ArcGIS.Geometry.esriSRUnit2Type), iGeo.CoordinateUnit.FactoryCode))
                    {
                        ESRI.ArcGIS.Geometry.esriSRUnit2Type myUnit2 = (ESRI.ArcGIS.Geometry.esriSRUnit2Type)Enum.ToObject(typeof(ESRI.ArcGIS.Geometry.esriSRUnit2Type), iGeo.CoordinateUnit.FactoryCode);
                    }
                    conversionFactor = iGeo.CoordinateUnit.ConversionFactor;
                }
            }
            else
            {
                srFactory.CreateESRISpatialReference(spatialReference.WKT, out iSpatialReference, out cBytesRead);

                if (iSpatialReference is ESRI.ArcGIS.Geometry.IProjectedCoordinateSystem)
                {
                    //units = ((ESRI.ArcGIS.Geometry.IProjectedCoordinateSystem)iSpatialReference).CoordinateUnit.Name;
                }
                else if (iSpatialReference is ESRI.ArcGIS.Geometry.IGeographicCoordinateSystem)
                {
                    //units = ((ESRI.ArcGIS.Geometry.IGeographicCoordinateSystem)iSpatialReference).CoordinateUnit.Name;

                }
            }

            System.Runtime.InteropServices.Marshal.ReleaseComObject(srFactory);

            return conversionFactor;

        }

        /// <summary>
        /// Gets the envelope at scale.
        /// </summary>
        /// <param name="scale">The scale.</param>
        /// <param name="extent">The extent.</param>
        /// <param name="dpi">The dpi.</param>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public static EnvelopeN GetEnvelopeAtScale(double scale, EnvelopeN extent, int dpi, int width, int height)
        {
            bool isDegrees = false;
            double conversionFactor = SpatialUtilities.GetMapUnitsFromSpatialReference(extent.SpatialReference, out isDegrees);

            PointN centerPoint = new PointN();
            centerPoint.X = extent.XMin + ((extent.XMax - extent.XMin) / 2);
            centerPoint.Y = extent.YMin + ((extent.YMax - extent.YMin) / 2);
            EnvelopeN newExtent = GetEnvelopeAtScale(scale, centerPoint, dpi, width, height, conversionFactor, isDegrees);
            newExtent.SpatialReference = extent.SpatialReference;
            return newExtent;
        }

        /// <summary>
        /// Gets the envelope at scale.
        /// </summary>
        /// <param name="scale">The scale.</param>
        /// <param name="centerPoint">The center point.</param>
        /// <param name="dpi">The dpi.</param>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        /// <param name="conversionFactor">The conversion factor.</param>
        /// <param name="isDegrees">if set to <c>true</c> [is degrees].</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public static EnvelopeN GetEnvelopeAtScale(double scale, PointN centerPoint, int dpi, int width, int height, double conversionFactor, bool isDegrees)
        {
            if (isDegrees) conversionFactor = 1;

            //  In order to determine a new map extent, the Map Scale Factor must be calculated using the following equation: 
            //		Map Scale Factor = Reference Scale / (ppi * constant) 
            //		where: 
            //		ppi = dpi 
            //		constant = 39.3701 inches per meter or 12 inches per foot
            double scaleFactor = scale / (dpi * (39.3701 * conversionFactor));

            if (isDegrees)
            {
                //If the scale is in decimal degrees. For these calculations, the Earth is assumed to be an exact sphere with a circumference 
                // of 40030.174 km. One degree is 111.195 km (40030.174/360 degrees), or 111195 meters. To calculate the number of degrees, 
                // first do the calculation for meters (#1). Next, divide the result by 111195. For example, if the number of meters per pixel 
                // is 6.36, the number of degrees is 6.36 / 111195, or 0.0000571968
                scaleFactor = scaleFactor / 111195;
            }

            // The Map Scale Factor can be used to calculate the width and height (in map units) of the new map extent. 
            //	  Multiply the Map Scale Factor by the map image width and height to return the new map extent width and height: 
            double mapExtentWidth = scaleFactor * width;
            double mapExtentHeight = scaleFactor * height;

            // Now create a new extent using the width/height.
            EnvelopeN newExtent = new EnvelopeN();
            newExtent.XMin = centerPoint.X - (mapExtentWidth / 2);
            newExtent.XMax = centerPoint.X + (mapExtentWidth / 2);
            newExtent.YMin = centerPoint.Y - (mapExtentHeight / 2);
            newExtent.YMax = centerPoint.Y + (mapExtentHeight / 2);
            return newExtent;
        }

        /// <summary>
        /// Gets the snapped envelope.
        /// </summary>
        /// <param name="resources">The resources.</param>
        /// <param name="extent">The extent.</param>
        /// <param name="scale">The scale.</param>
        /// <param name="dpi">The dpi.</param>
        /// <param name="widthPixels">The width pixels.</param>
        /// <param name="heightPixels">The height pixels.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public static EnvelopeN GetSnappedEnvelope(List<ExportMapResource> resources, EnvelopeN extent, double scale, int dpi, int widthPixels, int heightPixels)
        {

            for (int i = 0; i < resources.Count; i++)
            {
                WorldViewSolutions.ArcGISServer.SOE.DataSources.IMapDResource resource = resources[i].Resource;

                if (resource.IsCached)
                {
                    ESRI.ArcGIS.ADF.ArcGISServer.TileCacheInfo cacheInfo = resource.TileCacheInfo;
                    ESRI.ArcGIS.ADF.ArcGISServer.LODInfo[] infos = cacheInfo.LODInfos;

                    double snappedScale = 0;
                    double res = 0;
                    for (int x = 0; x < infos.Length; x++)
                    {
                        ESRI.ArcGIS.ADF.ArcGISServer.LODInfo li = infos[x];
                        if (snappedScale == 0)
                        {
                            snappedScale = li.Scale;
                            res = li.Resolution;
                        }
                        else
                        {
                            if (Math.Abs(scale - li.Scale) < Math.Abs(scale - snappedScale))
                            {
                                snappedScale = li.Scale;
                                res = li.Resolution;
                            }
                        }
                    }
                    scale = snappedScale;
                    break;
                }
            }

            PointN centerPoint = new PointN();
            centerPoint.X = extent.XMin + ((extent.XMax - extent.XMin) / 2);
            centerPoint.Y = extent.YMin + ((extent.YMax - extent.YMin) / 2);

            bool isDegrees = false;
            double mapUnitType = SpatialUtilities.GetMapUnitsFromSpatialReference(extent.SpatialReference, out isDegrees);

            EnvelopeN newExtent = GetEnvelopeAtScale(scale, centerPoint, dpi, widthPixels, heightPixels, mapUnitType, isDegrees);
            newExtent.SpatialReference = extent.SpatialReference;
            return newExtent;
        }

        /// <summary>
        /// Toes the screen unit.
        /// </summary>
        /// <param name="extent">The extent.</param>
        /// <param name="mapWidth">Width of the map.</param>
        /// <param name="mapHeight">Height of the map.</param>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public static System.Drawing.Point ToScreenUnit(EnvelopeN extent, int mapWidth, int mapHeight, double x, double y)
        {
            double extentWidth = extent.XMax - extent.XMin;
            double extentHeight = extent.YMax - extent.YMin;
            int left = Convert.ToInt32(Math.Round((x - extent.XMin) / (extentWidth / mapWidth)));
            int top = Convert.ToInt32(mapHeight - Math.Round((y - extent.YMin) / (extentHeight / mapHeight)));
            return new System.Drawing.Point(left, top);
        }

        /// <summary>
        /// Use this method when x=0, y=0 means the bottom left corner of the image.
        /// </summary>
        /// <param name="extent">The extent.</param>
        /// <param name="mapWidth">Width of the map.</param>
        /// <param name="mapHeight">Height of the map.</param>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public static System.Drawing.Point ToScreenUnitInverse(EnvelopeN extent, int mapWidth, int mapHeight, double x, double y)
        {
            double extentWidth = extent.XMax - extent.XMin;
            double extentHeight = extent.YMax - extent.YMin;
            int left = Convert.ToInt32(Math.Round((x - extent.XMin) / (extentWidth / mapWidth)));
            int top = Convert.ToInt32( Math.Round((y - extent.YMin) / (extentHeight / mapHeight)));
            return new System.Drawing.Point(left, top);
        }
    }
}